'''
Created on Nov 28, 2009

@author: johnathon, matt, matt
'''
import Environment, Main, pygame, random, copy, math #@UnresolvedImport
from CommunicationSystem import CommunicationSystem
import Board


class Nanobot(Environment.WorldObject):
    # TODO: in some spots loc is [y,x], in others (y,x) this is BAD, as == fails
    # For some reason I am not sure of, even if both sides of the == are tuples, it will
    # still fail. Any time we see this, we need to just compare each element 
    # individually - jts

    commSystem = CommunicationSystem()

    def __init__(self, loc, gridsize, worldsize, randSeed, id,  flag = 0, visionRadius = 3):
        ''' Initialize the Nanobot.
            Invoke the WorldObject superclass constructor, and 
            use that to keep track of basic things like the sprite
        '''
        Environment.WorldObject.__init__(self, loc, gridsize, col = (0, 0, 255), solid = True)
        self.visionRadius = visionRadius
        self.internalGrid = Board.getEmptyGrid(worldsize/gridsize)
        self.masterBoard = None
        self.flag = flag
        self.actionQueue = []
        self.id = id
        self.time = 0
        self.state = "explore"
        self.randNumGen = random.Random()
        self.randNumGen.seed(randSeed)
        self.viewableObjects = []
        self.incomingComms = []
        self.targetlist = [((10,10),(37,10)),((45,45),(37,9)),((15,32),(37,8)),((3,26),(38,10)),((20,20),(38,8)),((15,45),(39,10)),((20,35),(39,9)),((30,40),(39,8)),((40,40),(38,9))]
        self.previousCommReceipients = []
        self.workcounter = -1
        self.target = None
        self.meetingLoc = None
        self.numBots = 0
        self.botSet = set([self])
        self.blockSet = set([])
        self.goalSet = set([])
        self.assignments = []
        self.inconsistent = None
        self.plan = None
        self.planNum = -1
        self.taskList = []
        self.currentTask = None
        self.waitAt = None
        self.nextBot = None
        self.waitCounter = 0
        self.receivedPlan = []
        self.readyWait = 0
        self.nextGuys = []
        self.nextGuy = None
        self.waitAgain = 0
        
    def __repr__(self):
        return "Nanobot " + str(self.id)
    
    def nanoVision(self):
        ''' Performs Nanobot vision.
            Marks all of the squares within the vision radius as seen.
            Retrieves the information on what is in those squares
            from the actual representation of the internalGrid and inserts that
            information into the internalGrid.
        '''
        self.viewableObjects = []
        locs = self.masterBoard.locsInRadius(self.loc, self.visionRadius)
        for loc in locs:
            row, col = loc
            square = self.internalGrid[row][col]
            square.contents = self.masterBoard.objectsAt((row,col))
            square.time = self.time
            square.viewed = True
            
            if square.contents:
                for obj in square.contents:
                    if obj is not self:
                        self.viewableObjects.append(obj)
                    if isinstance(obj, Environment.Block):
                        self.blockSet.add(obj)
                    elif isinstance(obj, Environment.Goal):
                        self.goalSet.add(obj)
    
    def generateMoves(self, loc, moveToLoc):
        ''' Fills self.actionQueue with the function calls needed
            for something to move in the straightest path from 
            loc to the destination specified in moveToLoc.
            The function raises an error if the Nanobot attempts to move
            off of the internalGrid.
        '''
        if not self.masterBoard.validLoc(moveToLoc):
            raise ValueError("Loc not within valid range: Val: %s - BoardSize: %s" % 
                             (moveToLoc, self.masterBoard.size))
        finalRow, finalCol = moveToLoc
        tempRow, tempCol = loc
        actionlist = []
        while tempRow != finalRow or tempCol != finalCol:
            if tempRow < finalRow:
                tempRow += 1
            elif tempRow > finalRow:
                tempRow += -1
            if tempCol < finalCol:
                tempCol += 1
            elif tempCol > finalCol:
                tempCol += -1
            
            if self.masterBoard.validLoc((tempRow, tempCol)):
                actionlist.append((self.move, (tempRow, tempCol)))
            else:
                raise ValueError("Loc not within valid range: Val: %s - BoardSize: %s" % 
                                 (moveToLoc, self.masterBoard.size))
        return actionlist
    
    def pathFind(self, obj, moveToLoc):
        ''' Finds a path around an object blocking a given path. Essentially
            the bot attempts to move around the object by going to the left.
        '''
        if self.masterBoard.blockedLoc(moveToLoc):
            direction = (moveToLoc[0]-obj.loc[0],moveToLoc[1]-obj.loc[1])
            if not obj.actionQueue:
                goal = (moveToLoc[0]+direction[0],moveToLoc[1]+direction[1])
            else:
                goal = obj.actionQueue[0][1]
            
            if abs(direction[0])+abs(direction[1]) == 1:        
                movelist = [(0,-1),(-1,0),(-1,0),(0,1),(0,1),(1,0),(1,0),(0,-1)]
            else:
                movelist = [(-1,0),(-1,0),(0,1),(0,1),(1,0),(1,0),(0,-1),(0,-1)]

            if direction == (0,1) or direction == (1,1):
                movelist.extend(movelist[0:2])
                del(movelist[0:2])
            if direction == (1,0) or direction == (1,-1):
                movelist.extend(movelist[0:4])
                del(movelist[0:4])
            if direction == (0,-1) or direction == (-1,-1):
                movelist.extend(movelist[0:6])
                del(movelist[0:6])
            
            loc = (obj.loc[0], obj.loc[1])
            addmoves = []
            j = -1    
            while not (loc[0] == goal[0] and loc[1] == goal[1]):
                j += 1
                if j > len(movelist)-1:
                    return obj.loc
                loc = self.addTuple(loc,movelist[j])
                if self.masterBoard.validLoc(loc):
                    addmoves.append((self.move, loc))
                else:
                    return obj.loc
                
            addmoves.extend(obj.actionQueue[1:])
            obj.actionQueue = addmoves[1:]
            try:
                moveToLoc = addmoves[0][1]
            except IndexError:
                return    
            if self.masterBoard.validLoc(moveToLoc) and not self.masterBoard.blockedLoc(moveToLoc):
                return moveToLoc
            return obj.loc
        
    
    def move(self, moveToLoc):
        ''' Moves the Nanobot from the current location to the location specified
            in moveToLoc. Updates the masterBoard to make sure that the Nanobot appears
            in the correct place on the GUI.
        '''
        if not self.masterBoard.validLoc(moveToLoc):
            raise ValueError("Tried to move to an invalid spot: Val: %s - BoardSize: %s" 
                             %  (moveToLoc, self.masterBoard.size))

        if self.masterBoard.blockedLoc(moveToLoc):
            moveToLoc = self.pathFind(self, moveToLoc)
            if moveToLoc == False or moveToLoc == None:
                moveToLoc = self.loc
        
        try:
            self.internalGrid[self.loc[0]][self.loc[1]].contents.remove(self)
        except ValueError:
            pass
            
        self.loc = moveToLoc
        
        self.internalGrid[self.loc[0]][self.loc[1]].contents.append(self)
    
    def unknownCheck(self, loc):
        ''' Returns the number of unknown squares that would be
            viewed if the bot moved to loc.
        '''
        locs = self.masterBoard.locsInRadius(loc, self.visionRadius)
        unknownCount = 0
        for y, x in locs:
            if not self.internalGrid[y][x].viewed and self.masterBoard.validLoc((y,x)):
                unknownCount += 1
        return unknownCount
    
    def nanoSearch(self):
        ''' Returns the best location that the Nanobot can move to to maximize 
            exploration of unseen spaces. If none of the adjacent moves will
            explore a space, then the Nanobot moves to the nearest unknown space.
        '''
        maxUnknowns = 0
        bestmove = []
        for loc in self.masterBoard.locsInRadius(self.loc, 1):
            testY, testX = loc
            if self.masterBoard.validLoc((testY, testX)):
                testUnknowns = self.unknownCheck((testY,testX))
                if testUnknowns > maxUnknowns:
                    maxUnknowns = testUnknowns
                    bestmove = [(testY, testX)]
                elif testUnknowns == maxUnknowns:
                    bestmove.append((testY, testX))
        if maxUnknowns == 0:
            return self.findUnknown()
        else:
            return random.choice(bestmove)
    
    def findUnknown(self):
        ''' Returns the nearest unknown square to the Nanobot.
            If there are no more unknowns spaces, the function
            returns the current location of the Nanobot.
        '''
        dist = 1
        while dist < self.masterBoard.size:
            for i in range(-dist, dist):
                for j in range(-dist, dist):
                    y = self.loc[0] + i
                    x = self.loc[1] + j
                    
                    if self.masterBoard.validLoc((y,x)):
                        if not self.internalGrid[y][x].viewed:
                            return (y, x)
            dist = dist+1
        return self.loc

    def performAction(self):
        ''' The main Nanobot loop.
            Handles making the Nanobot do everything it is supposed to do during
            its timestep.
            We perform Nanovision and then check if their are actions in the actionQueue.
            If there are, we perform one of the actions, if there are not, we figure out
            what fill the actionQueue with the actions we want to perform next, and then
            execute the first one.
        '''
        self.nanoVision()
        self.resolveIncomingCommunication()
        self.attemptOutgoingCommunication()
        
        if not self.actionQueue:
            moveToLoc = None
            if self.state == "explore":
                moveToLoc = self.nanoSearch()
                if moveToLoc[0]==self.loc[0] and moveToLoc[1]==self.loc[1]:
                    self.state = "meet"
                else:
                    self.actionQueue = self.generateMoves(self.loc, moveToLoc)
            
            elif self.state == "wait":
                # At meeting location, checking to see if everyone is there.
                pass
                    
            elif self.state == "endrun":
                self.image.fill((100,100,100))
            
            elif self.state == "meet":
                self.numBots = self.masterBoard.botCount
                self.meetingLoc = self.findMeetingLocation()
                self.actionQueue = self.generateMoves(self.loc, self.meetingLoc)
                self.state = "wait"
                           
            elif self.state == "ready":
                self.readyWait += 1
                if self.readyWait > 10:
                    if not self.assignments:
                        (self.assignments, self.inconsistent) = self.assignGoals()
                    elif self.inconsistent > 0:
                        self.inconsistent = self.assignmentPermute()
                        
                    if self.inconsistent == 0:
                        self.testPlan = self.createPlan()
                        self.plan = self.testPlan[:]
                        self.state = "deciding"
                else:
                    pass
            elif self.state == "deciding":
                pass
            
            elif self.state == "endcheck":
                self.actionQueue = self.generateMoves(self.loc,self.meetingLoc)
                    
            elif self.state == "positioning":
                self.accomplish()
                self.actionQueue = self.nanoPush(self.currentTask[0].loc,self.waitAt)
                self.target = self.waitAt
                # state gets set to "working" here
                  
            elif self.state == "done":
                
                if ((self.plan.index(self.currentTask)+1) % self.numBots) == 1 and not (self.currentTask[0].loc[0] == self.currentTask[1].loc[0] and self.currentTask[0].loc[1] == self.currentTask[1].loc[1]):
                    self.state = "finishing"
                elif self != self.plan[0][2] and not self.currentTask[0].loc == self.currentTask[1].loc:
                    pass
                
                if self.currentTask[0].loc[0] == self.currentTask[1].loc[0] and self.currentTask[0].loc[1] == self.currentTask[1].loc[1]:
                    if self.nextGuy and (self.plan.index(self.currentTask)+1) % self.numBots != 0:
                        self.state = "informing"
                    else:
                        if self.taskList:
                            self.state = "return"
                            self.botSet = set([self])
                            self.actionQueue = self.generateMoves(self.loc, self.meetingLoc)
                        else:
                            self.state = "endrun"
                            self.actionQueue = self.generateMoves(self.loc,self.meetingLoc)
                pass        
                
            elif self.state == "working":
                self.actionQueue = self.nanoPush(self.currentTask[0].loc,self.target)
                
            elif self.state == "finishing":
                self.actionQueue = self.nanoPush(self.currentTask[0].loc,self.currentTask[1].loc)
                self.target = self.currentTask[1].loc
                
            elif self.state == "informing":
                self.nextBot = self.nextGuy[2]
                self.nanoPush(self.nextGuy[0].loc,self.nextGuy[1].loc)
                self.state = "informing"
                try:
                    self.actionQueue = self.generateMoves(self.loc,self.nextGuy[0].actionQueue[-8][1])
                except IndexError:
                    self.actionQueue = self.generateMoves(self.loc,self.nextGuy[0].loc)
                self.nextGuy[0].actionQueue = []
            
            elif self.state == "end":
                self.actionQueue = self.generateMoves(self.loc, self.meetingLoc)
                    
        if self.actionQueue:
            (func, params) = self.actionQueue.pop(0)
            func(params)
        self.time += 1
    
    def resolveIncomingCommunication(self):
        ''' Pulls the functions out of the incoming communication and runs them
            with the input also supplied by the communication
        '''
        for function, object in self.incomingComms:
            function(self, object)
            
        del self.incomingComms[:]
            
    def attemptOutgoingCommunication(self):
        ''' Finds the nanobots in the list of viewable objects, and then determines
            what communications should be sent to them
        '''
        visibleNanobots = [obj for obj in self.viewableObjects if isinstance(obj, Nanobot)]
            
        if self.state == "deciding":
            self.waitCounter += 1
            if self.receivedPlan == []:
                pass
            elif (self.receivedPlan[1] > self.planNum):
                (self.plan, self.planNum) = self.receivedPlan
                self.waitCounter = 0
                self.receivedPlan = []
            elif self.receivedPlan[1] <= self.planNum:
                self.receivedPlan = []
                
        if self.state == "wait":
            if len(self.botSet) == self.numBots:
                self.state = "ready"
                
        for nano in visibleNanobots:
            if self.state == "explore":
                self.commSystem.sendMessage(nano, self.commSystem.mergeMap, self.internalGrid)
            
            elif self.state == "wait" and not self.actionQueue:
                if self.masterBoard.isAdjacent(self.loc, nano.loc):
                    self.commSystem.sendMessage(nano, self.commSystem.stopMovement, None)    
                self.commSystem.sendMessage(nano, self.commSystem.mergeSet, (self, self.botSet, "botSet"))                   
                self.commSystem.sendMessage(nano, self.commSystem.mergeSet, (self, self.blockSet, "blockSet"))
                self.commSystem.sendMessage(nano, self.commSystem.mergeSet, (self, self.goalSet, "goalSet"))
            
            elif self.state == "return" and not self.actionQueue:
                print "test"
                if self.masterBoard.isAdjacent(self.loc,nano.loc):
                    self.commSystem.sendMessage(nano, self.commSystem.stopMovement, None)
                self.commSystem.sendMessage(nano, self.commSystem.mergeSet, (self, self.botSet, "botSet"))
            
            elif self.state == "endrun" and not self.actionQueue:
                if self.masterBoard.isAdjacent(self.loc,nano.loc):
                    self.commSystem.sendMessage(nano, self.commSystem.stopMovement, None)
            
            elif self.state == "deciding":
                self.commSystem.sendMessage(nano, self.commSystem.stateSet, ("receivedPlan", (self.plan, self.planNum)))
            
            elif self.state == "informing":
                if nano == self.nextBot:
                    self.commSystem.sendMessage(nano, self.commSystem.stateSet, ("state", "finishing"))
                    if self.taskList:
                        self.state = "return"
                    else:
                        self.state = "endrun"
                    self.botSet = set([self])
                    self.actionQueue = self.generateMoves(self.loc,self.meetingLoc)
                    
        if self.state == "return":
            if len(self.botSet) == self.numBots:
                self.waitAgain += 1
                if self.waitAgain == 3:
                    self.state = "positioning"
                    self.waitAgain = 0
                   
        if self.state == "deciding":
            if self.waitCounter >= 5*self.numBots+1:
                self.state = "positioning"
                count = -1
                for task in self.plan:
                    count += 1
                    if task[2] == self:
                        self.taskList.append(task)
                        try:
                            self.nextGuys.append(self.plan[count+1])
                        except IndexError:
                            pass
            

                    
    def findMeetingLocation(self):
        ''' Generates a meeting location for the nanobots by cycling through
            meeting locations generated by a random number generator until 
            a location that has 4x4 clear spots are found. This is deterministic
            because each bot uses the same seed in its random number generator.
        '''
        clearRadius = 4
        while True:
            randomLocation = (self.randNumGen.randint(clearRadius, self.masterBoard.size - clearRadius), 
                              self.randNumGen.randint(clearRadius, self.masterBoard.size - clearRadius))
            locs = self.masterBoard.locsInRadius(randomLocation, clearRadius)
            blockingObject = False
            for y, x in locs:
                # TODO: Maybe allow Nanobots to convene overtop of non-solid objects
                contents = [obj for obj in self.internalGrid[y][x].contents 
                            if not isinstance(obj, Nanobot)]
                if contents:
                    blockingObject = True
                    break
            if blockingObject:
                continue
            return randomLocation
    
    def nanoPush(self, objloc, objdest):
        ''' The Nanobot pushes a block.
            First we test if the Nanobot is next to the block it is trying to push.
            If it is, then the Nanobot calls push on the block, moving the block and then
            moves into the square that the block was in.
        '''
        obj = self.masterBoard.objectsAt(objloc)[0]
        nextmove = []
        
        if self.state == "positioning":
            if objloc == objdest:
                return
        
        if self.state == "working" and obj == self.currentTask[0]:
            if abs(objloc[0]-self.loc[0]) > 1 or abs(objloc[1]-self.loc[1]) > 1:
                return
 #               raise ValueError("Bot at %s tried to push block at %s" % (self.loc,objloc))
            obj.push(self.loc)
            if obj.loc == objdest:
                self.state = "done"
                return self.generateMoves(self.loc,objloc)
#            self.target = obj.loc
            obj.actionQueue = self.generateMoves(obj.loc,objdest)
            nextmove = self.generateMoves(self.loc,objloc)
        
        if isinstance(obj, Environment.Block) and not obj.actionQueue:
            obj.actionQueue = self.generateMoves(objloc,objdest)
            self.state = "working"  
        
        try:
            nextBlockMove = obj.actionQueue.pop(0)[1]
        except IndexError:
            nextBlockMove = obj.loc
        if self.masterBoard.blockedLoc(nextBlockMove):
            nextBlockMove = self.pathFind(obj, nextBlockMove)
        nextBotMove = (2*obj.loc[0]-nextBlockMove[0],2*obj.loc[1]-nextBlockMove[1])
        if self.masterBoard.blockedLoc(nextBotMove):
            locs = self.masterBoard.locsInRadius(nextBotMove,1)
            for loc in locs:
                if abs(loc[0]-obj.loc[0]) < 2 and abs(loc[1]-obj.loc[1]) < 2 and not self.masterBoard.blockedLoc(loc):
                    nextBotMove = loc

        if nextmove:
            nextmove.extend(self.generateMoves(objloc,nextBotMove))
            return nextmove
        return self.generateMoves(self.loc,nextBotMove)
    
    def assignGoals(self):
        ''' Assigns nanobots to each task '''
        assignments = []
        goalsAdded = set([])
        inconsistent = 0
        # just setting goals = self.goalSet etc. causes goals and blocks
        # to point to the object they are getting rather than copy it.
        goals = list(self.goalSet)
        blocks = list(self.blockSet)
        while goals:
            nextGoal = random.choice(goals)
            goals.remove(nextGoal)
            nextBlock = random.choice(blocks)
            blocks.remove(nextBlock)
            assignments.append([nextBlock, nextGoal])
            goalsAdded.add(nextGoal)
            
            # Inconsistency checking: Checks to see if the order of block assignment has
            # blocked in a goal state.
            numBlocked = 0
            if len(goalsAdded) > 3:
                for (y,x) in [(-1,0),(0,1),(1,0),(0,-1)]:
                    test = self.addTuple(nextGoal.loc,(y,x))
                    objlist = self.masterBoard.objectsAt(test)
                    for obj in objlist:
                        if obj in goalsAdded or obj.solid == True:
                            numBlocked += 1          
                if numBlocked == 4:
                    inconsistent += 1                          
        return (assignments, inconsistent)
            
    def assignmentPermute(self): 
        ''' Permutes the current task plan '''
        p = random.randint(0, len(self.goalSet)-1)
        q = random.randint(0,len(self.goalSet)-1)
        swap1 = self.assignments[p][1]
        swap2 = self.assignments[q][1]
        self.assignments[p][1] = swap2
        self.assignments[q][1] = swap1
        
        inconsistent = 0
        for i in range(4,len(self.goalSet)):
            numBlocked = 0
            prevGoals = set([])
            for j in range(0,i):
                prevGoals.add(self.assignments[j][1])
            for (y,x) in [(-1,0),(0,1),(1,0),(0,-1)]:
                test = self.addTuple(self.assignments[i][1].loc,(y,x))
                objlist = self.masterBoard.objectsAt(test)
                for obj in objlist:
                    if obj in prevGoals or obj.solid == True:
                        numBlocked += 1
            if numBlocked == 4:
                inconsistent += 1
                
        return inconsistent
    
    def createPlan(self):
        ''' Creates an initial task plan '''
        bots = list(self.botSet)
        plan = []
        
        while self.assignments:
            try:
                bot = random.choice(bots)
            except IndexError:
                bots = list(self.botSet)
                bot = random.choice(bots)
            bots.remove(bot)    
            assign = self.assignments.pop(0)
            assign.append(bot)
            plan.append(assign)
            
        self.state = "planned"
        self.planNum = random.random()
        return plan
            
    def accomplish(self):
        ''' This method figures out what the nanobot would have to do to move
            so that it can enter the wait state. That is, where does it have to
            push the block to, so that it can begin waiting.
        '''
        self.currentTask = self.taskList.pop(0)
        try:
            self.nextGuy = self.nextGuys.pop(0)
        except IndexError:
            self.nextGuy = []
        block = self.currentTask[0]
        goal = self.currentTask[1]
        self.actionQueue = self.nanoPush(block.loc,goal.loc)
        try:
            self.waitAt = block.actionQueue[-8][1]
        except IndexError:
            self.waitAt = block.loc
        block.actionQueue = []            
        self.state = "positioning"